
# Copyright 2009 David Lee Miller
#
# This file is part of POSSM (Python Open Source Scanning Microscopy).
#
# POSSM is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# POSSM is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License         
# along with POSSM.  If not, see <http://www.gnu.org/licenses/>.
#                                                                           
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

                              #_ _   _____       _             __
                             #| (_) |_   _|     | |           / _|
  #___ ___  _ __ ___   ___  __| |_    | |  _ __ | |_ ___ _ __| |_ __ _  ___ ___
 #/ __/ _ \| '_ ` _ \ / _ \/ _` | |   | | | '_ \| __/ _ \ '__|  _/ _` |/ __/ _ \
#| (_| (_) | | | | | |  __/ (_| | |  _| |_| | | | ||  __/ |  | || (_| | (_|  __/
 #\___\___/|_| |_| |_|\___|\__,_|_| |_____|_| |_|\__\___|_|  |_| \__,_|\___\___|
#################################################################################

#This file defines the read a write funtions for the
#NI-6030/pci-mio-16xe-10 Card using the Comedi library. DLM

from controller import n,ws,os,struct
import comedi


class Device(object):
	'''
	A class for basic comedi recognition of devices and subdevices. Also collects hardware
	specific attributes of the device.
	
	Arguments:
	dev_no -- the comedi device number
	
	Optional Arguments:
	dig -- set as False if device has no digital IO capabilities
	ai -- set as False if device has no analog input capabilities
	ao -- set as False if device has no analog output capabilities
	fd -- set as True if using device with hardware buffer capabilities
	'''
	
	DEV_NO = None
	DEV = None
	FD = None
	DEV_FD = None
	SUB_DIO = None
	SUB_AI = None
	SUB_AO = None
	AO_BITS = None
	AI_BITS = None
	
	CAL_DIFF = comedi.AREF_DIFF
	CAL_GROUND = comedi.AREF_GROUND
	COM_DIG = comedi.COMEDI_SUBD_DIO
	COM_AI = comedi.COMEDI_SUBD_AI
	COM_AO  = comedi.COMEDI_SUBD_AO
	
	def __init__(self,dev_no,dig=True,ai=True,ao=True,fd=False):
		'''
		Initialzation of devices. dig, ai and ao are all set to
		True. If the board does not have one of these devices, mark
		it false.
		
		'''
		
		self.DEV_NO = dev_no
		self.PATH = '/dev/comedi'+str(self.DEV_NO)
		
		self.FindDevice()
		if fd and self.DEV: self.OpenFileDescriptor()
		if self.DEV:
			self.FindSubdevices(dig,ai,ao)
			#self.ApplyCalibration()
			self.GetBits()
	
	def FindDevice(self):
		'''
		Locates the device from device file.
		
		'''
		dev_open = comedi.comedi_open(self.PATH)
		if not dev_open: raise "error opening device"
		else: self.DEV = dev_open
	
	def OpenFileDescriptor(self):
		'''
		Opens a file descriptor for use with devices
		that have on board clocks. Required for block read/write
		commands that have a data buffer.
		
		'''
		fd = comedi.comedi_fileno(self.DEV)
		if fd <= 0: raise "error obtaining file descriptor"
		else: self.FD = fd
	
	def FindSubdevices(self,dig,ai,ao):
		'''
		Finds the needed subdevices for the device board.
		
		'''
		if dig:
			digSub = -1
			digSub = comedi.comedi_find_subdevice_by_type(self.DEV, self.COM_DIG, 0)
			if digSub < 0: raise "error finding proper DIO subdevice"
			else: self.SUB_DIO = digSub
		else: pass
		
		if ai:
			aiSub = -1
			aiSub = comedi.comedi_find_subdevice_by_type(self.DEV, self.COM_AI, 0)
			if aiSub < 0: raise "error finding proper AI subdevice"
			else: self.SUB_AI = aiSub
		else: pass
		
		if ao:
			aoSub = -1
			aoSub = comedi.comedi_find_subdevice_by_type(self.DEV, self.COM_AO, 0)
			if aoSub < 0: raise "error finding proper AO subdevice"
			else: self.SUB_AO = aoSub
		else: pass
	
	def ApplyCalibration(self):
		'''
		Calibrates the analog subdevices on the board. Must be used in
		conjunction with the external comedi_calibrate function.
		
		'''
		
		if self.SUB_AI != None:
			cal = -1
			cal = comedi.comedi_apply_calibration(self.DEV,self.SUB_AI,0,0,self.CAL_DIFF,None)
			if cal < 0: raise "error calibrating device"
		else: pass
		
		#if self.SUB_AO != None:
			#cal = -1
			#cal = comedi.comedi_apply_calibration(self.DEV,self.SUB_AO,0,0,self.CAL_GROUND,None)
			#if cal < 0: raise "error calibrating device"
		#else: pass
	
	def GetBits(self):
		'''
		Finds the number of bits on the board for the various subdevices.
		
		'''
		if self.SUB_AI != None:
			self.AI_MAXDATA = comedi.comedi_get_maxdata(self.DEV,self.SUB_AI,0)
			num = 2
			bits = 0
			while num < self.AI_MAXDATA:
				bits += 1
				num = 2**bits
			self.AI_BITS = bits
		if self.SUB_AO != None:
			self.AO_MAXDATA = comedi.comedi_get_maxdata(self.DEV,self.SUB_AO,0)
			num = 2
			bits = 0
			while num < self.AO_MAXDATA:
				bits += 1
				num = 2**bits
			self.AO_BITS = bits

class DeviceFunctions(Device):
	'Low level class for basic device functionality.'
	
	DIO_OUT = comedi.COMEDI_OUTPUT
	DIO_IN = comedi.COMEDI_INPUT
	#AI_BITS = 16	#for other bit resolutions, change upon instantiation of the class
	#AO_BITS = 16
	
	buf_size = 4096
	extralen = n.add(n.zeros((buf_size)),1)
	freq = 200000.0
	
	def __init__(self,dev_no,dig=True,ai=True,ao=True,fd=False,corr=1.):
		Device.__init__(self,dev_no,dig=dig,ai=ai,ao=ao,fd=fd)
		self.DIO_BITS = comedi.comedi_get_n_channels(self.DEV,self.SUB_DIO)
		if ao and fd: comedi.comedi_set_buffer_size(self.DEV,self.SUB_AO,self.buf_size)
		self.RNG_CORR = corr
	
	def DigitalConfig(self,chan,output=True):
		'''
		Configures digital lines for input/output.
		
		Keyword Arguments:
		subd -- subdevice to perform the config on
		chan -- channel to configure
		output -- default configures device to output, using False is for input
		
		'''
		if output: direction = self.DIO_OUT
		else: direction = self.DIO_IN
		
		comedi.comedi_dio_config(self.DEV,self.SUB_DIO,chan,direction)
	
	def DIOWrite(self,chan,val):
		'''
		Writes a value to the specified channel on the subdevice.
			
		Keyword Arguments:
		subd -- subdevice to perform the config on
		chan -- channel to configure
		val -- value: 0/1 --> digital high or low
		
		'''
		
		comedi.comedi_dio_write(self.DEV,self.SUB_DIO,chan,val)
	
	def AOInteger(self,chan,rng,dac):
		'''
		Writes a single dac value to the board.
		
		Keyword Arguments:
		chan -- Analog Output Channel
		rng -- The Comedi AO range to use
		dac -- The 16-bit digital voltage value to write
		
		'''
		if dac < 0: dac = 0
		elif dac > (2**self.AO_BITS-1): dac = (2**self.AO_BITS-1)
		if not type(dac) == int: dac = int(dac)
		comedi.comedi_data_write(self.DEV,self.SUB_AO,chan,rng,self.CAL_GROUND,int(dac))
		return dac
	
	def AIInteger(self,chan,rng,diff=True):
		'''
		Takes a single reading on the selected channel and returns
		the value.
		
		Keyword Arguments:
		chan -- Analog Input Channel
		rng -- Analog Input Range to use
		
		'''
		if diff: calib = self.CAL_DIFF
		else: calib = self.CAL_GROUND
		
		val = comedi.comedi_data_read(self.DEV,self.SUB_AI,chan,rng,calib)
		return val
	
	def byte_convert(self,waveform):
		'''
		Converts a waveform to a string of 2-byte numbers.
		
		Keyword arguments:
		waveform -- the waveform in list format
		
		byte_convert takes a list of integers (range 0-65535) and returns
		a string of 2-byte numbers. The string returned is the correct format
		for writing to the comedi data buffer for DAC operations.
		
		'''
		data = []
		for i in range(len(waveform)):
			data.append(struct.pack('H',waveform[i]))
		return ''.join(data)
	
	def intn_trig(self):
		'''
		Sets the internal trigger on the NI/comedi device.
		
		Keyword arguments:
		subd -- the integer subdevice number
		
		Supplies an internal trigger to the subdevice number
		given by the function argument. Returns a 1 if successful
		and a -1 if it fails.
		
		'''
		data = comedi.chanlist(1)
		data[0] = 0
		
		insn = comedi.comedi_insn_struct()
		insn.insn = comedi.INSN_INTTRIG
		insn.subdev = self.SUB_AO
		insn.data = data
		insn.n = 1
		return comedi.comedi_do_insn(self.DEV,insn)
	
	def AOBlock(self,chan,rng,wvfrm):
		'''
		Does a DAC output on the NI/comedi device.
		
		Keyword arguments:
		wvfrm -- the waveform to output in 16-bit string format
		chan -- the integer channel number to output on
		
		The write_block function outputs the waveform given in the first argument.
		The argument takes a list which contains your waveform. The list must contain
		2-byte integers (a range of numbers 0-65535). Then it converts the data and
		outputs to the DAC channel specified by the second argument at a frequency
		set in the command structure.
		
		'''
		multiplier = data[-1:][0]
		data = self.byte_convert(data)
		datBytes = len(data)
		extral = self.byte_convert(n.multiply(self.extralen,multiplier).tolist())
		extra = extral[datBytes:self.buf_size+2*datBytes]
		if len(data) < 1024: data = data + extra
		n_chan = 1
		cmd = comedi.comedi_cmd_struct()
		cmd.subdev = self.SUB_AO
		cmd.flags = 0
		cmd.start_src = comedi.TRIG_INT
		cmd.start_arg = 0
		cmd.scan_begin_src = comedi.TRIG_TIMER
		cmd.scan_begin_arg = int(1e9 / (self.freq))
		cmd.convert_src = comedi.TRIG_NOW
		cmd.convert_arg = 0
		cmd.scan_end_src = comedi.TRIG_COUNT
		cmd.scan_end_arg = n_chan
		cmd.stop_src = comedi.TRIG_COUNT
		cmd.stop_arg = len(wvfrm)-1
		clist = comedi.chanlist(n_chan+1)
		
		cmd.chanlist = clist
		cmd.chanlist_len = n_chan
		clist[0] = comedi.cr_pack(chan, rng, comedi.AREF_GROUND)
		clist[1] = comedi.cr_pack(chan+1, rng, comedi.AREF_GROUND)
		err = comedi.comedi_command_test(self.DEV,cmd)
		if err < 0:
			comedi.comedi_perror("comedi_command_test")
	
		err = comedi.comedi_command(self.DEV,cmd)
		if err < 0:
			comedi.comedi_perror("comedi_command")
			
		m = os.write(fd,data[0:1024])
		ret = self.intn_trig()
		if ret < 0: raise 'internal trigger failed'
		if m < len(data):
			n=m
			while 1:
				if n < len(data):
					m = os.write(fd,data[n:])
					n += m
					if m < 0:
						raise 'write error'
					if m == 0:
						break
				else: m = 0
				if m == 0:
					break
	
	def AIBlock(self,chan,rng,nscans,ignore=10,diff=True):
		'''
		Reads a block of data from the NI/comedi device.
		
		Keyword arguments:
		nscans -- the number of scans (i.e. data collections) to perform
		chan -- the integer channel number to scan
		minval -- the minimum value the channel can read
		maxval -- the maximum value the channel can read
		ignore -- the number of scans to ignore before starting nscans
		
		Requires 4 arguments. The first is the number of points to average
		over. The second is the analog input channel number to sample on.
		Function returns the averaged value of the samples taken. The last
		two values are used so that comedi can determine the range it should
		use when reading the values. In order to get the best accuracy, comedi
		chooses the smalles possible range based on minval and maxval.
		
		''' 
		n_chan = 1
		if diff: calib = self.CAL_DIFF
		else: calib = self.CAL_GROUND
		
		freq = 100000.0
		cmd = comedi.comedi_cmd_struct()
		ret = comedi.comedi_get_cmd_generic_timed(self.DEV,self.SUB_AI,cmd,int(1.0e9/freq))
		if ret: raise "Error comedi_get_cmd_generic failed"
		clist = comedi.chanlist(n_chan+1)
		
		cmd.chanlist = clist
		cmd.chanlist_len = n_chan
		if cmd.stop_src==comedi.TRIG_COUNT: cmd.stop_arg=nscans+ignore
		
		clist[0] = comedi.cr_pack(chan, rng, calib)
		clist[1] = comedi.cr_pack(chan+1, rng, calib)
		
		ret = comedi.comedi_command_test(self.DEV,cmd)
		if ret<0: raise "comedi_command_test failed"
		ret = comedi.comedi_command_test(self.DEV,cmd)
		if ret<0: raise "comedi_command_test failed"
		if ret !=0:
			raise "Error preparing command"
		err = comedi.comedi_command(self.DEV,cmd)
		if err < 0:
			comedi.comedi_perror("comedi_command")
		
		datastr = ()
		while (1):
			data = os.read(self.FD,2)
			if len(data)==0:
				break
			n = len(data)/2 # 2 bytes per 'H'
			format = `n`+'H'
			datastr = datastr + struct.unpack(format,data)
		datAvg = int(n.average(n.array(datastr[ignore:])))
		return datAvg
	
	def PhysicalToInteger(self,subd,chan,rng,val):
		'''
		Takes a value in physical units, matches it to the given
		comedi range for the analog subdevice and converts it to a python
		integer.
		
		Keyword arguments:
		subd -- subdevice of the
		chan -- the channel that the data references
		rng -- the range that the data references
		val -- the value to convert in physical uints
		
		'''
		
		if subd == self.SUB_AI: maxint = 2**self.AI_BITS
		else: maxint = 2**self.AO_BITS
		
		COMEDI_RNG = comedi.comedi_get_range(self.DEV,subd,chan,rng)
		intval = comedi.comedi_from_phys(val/self.RNG_CORR,COMEDI_RNG,maxint-1)
		return int(intval)
	
	def IntegerToPhysical(self,subd,chan,rng,dac):
		
		if subd == self.SUB_AI: maxint = 2**self.AI_BITS
		else: maxint = 2**self.AO_BITS
		
		COMEDI_RNG = comedi.comedi_get_range(self.DEV,subd,chan,rng)
		physval = comedi.comedi_to_phys(dac,COMEDI_RNG,maxint-1)
		if physval != physval and dac==0: physval = COMEDI_RNG.min
		elif physval != physval and dac==65535: physval = COMEDI_RNG.max  #minimum value spits out 'nan', this returns the real min
		return physval*self.RNG_CORR

device0 = DeviceFunctions(ws.DEV0_NUMBER)
device1 = DeviceFunctions(ws.DEV1_NUMBER,ai=False,ao=True,corr=2.)
devices = [device0,device1]